package de.rosstauscher.jculfw;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Pattern;

/**********************************************************
 * A library to control a CUL/CUN device.
 * To use this you need to create a socket or serial connection 
 * and provide it's input and output streams to read from and 
 * write too. 
 * 
 * @see http://culfw.de/commandref.html
 * 
 * @author Rossi java@rosstauscher.de
 *********************************************************/

public class JCulFW {
	
	// We read and write always ASCII 
	private static final String ENCODING = "ASCII";
	
	// "End of message" terminator  
	private static final String EOM = "\r\n";

	/// USB charge modes supported
	public enum ChargeMode {WITH_500ma, WITH_100ma}; 
	
	/// Clock display / readout modes
	public enum ClockMode {DATE_ONLY, TIME_ONLY, BOTH}; 
	
	/// Modes for operating the LED
	public enum LEDMode {ON, OFF, BLINK}; 
	
	private BufferedWriter out; 
	private BufferedReader in; 
	
	private ThreadFactory threadFactory;
	private Thread listenerThread; 
	private List<IRawMessageProcessor> listener;
	
	/**********************************************************
	 * Creates a new control object connected to a CUL/CUN(O) via
	 * the given streams. This should be either socket streams or 
	 * serial connection or file streams for a CUL.
	 * @param in an InputStream to read messages from.
	 * @param out an OutputStream to write commands to.
	 *********************************************************/
	
	public JCulFW(InputStream in, OutputStream out) {
		super();
		this.threadFactory = Executors.defaultThreadFactory();
		this.listener = new ArrayList<IRawMessageProcessor>();
		setupIO(in, out);
	}

	/**********************************************************
	 * Build buffered reader and writer for IO operations.
	 * @param in an stream to read from.
	 * @param out an stream to write to.
	 *********************************************************/
	
	private void setupIO(InputStream in, OutputStream out) {
		try {
			this.in = new BufferedReader(new InputStreamReader(in, ENCODING));
			this.out = new BufferedWriter(new OutputStreamWriter(out, ENCODING));
		} catch (UnsupportedEncodingException e) {
			// Should never happen because ASCII is always supported
			throw new IllegalStateException(e);
		}
	}
	
	/**********************************************************
	 * Sends a line containing a command.
	 * The command line is terminated correctly automatically.
	 * @param commandToken the tokens to build the command from.
	 * @throws IOException on error.
	 *********************************************************/
	
	private void writeLine(String... commandToken) throws IOException {
		for (String t : commandToken) {
			out.write(t);
		}
		out.write(EOM);
		out.flush();
	}

	/**********************************************************
	 * Reads a message from the CUL / CUN
	 * This method will block until data is available.
	 * @return the message, null if the end of the stream is reached.
	 * @throws IOException on error.
	 *********************************************************/
	
	private String readLine() throws IOException {
		return in.readLine();
	}
	
	
	/**********************************************************
	 * Enable Asksin receiption.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void enableAskSinMode() throws IOException {
		writeLine("Ar");
	}
	
	/**********************************************************
	 * Ensures that a given input string is a valid Hex string of 
	 * the given length.
	 * @param token to check for validity.
	 * @param length is the defined length that is expected.
	 * @throws IllegalArgumentException on invalid input.
	 *********************************************************/
	
	private void assertHex(String token, int length) {
		if (token == null || token.length() != length) {
			throw new IllegalArgumentException("Input invalid ("+token+"). Must have length "+length);
		}
		if (!Pattern.matches("[0-9A-F]*", token.toUpperCase())) {
			throw new IllegalArgumentException("Input hex format ("+token+"). Must be [0-9A-F]*");
		}
	}
	
	/**********************************************************
	 * Send an Asksin message.
	 * @param length
	 * @param messageCounter
	 * @param controlByte
	 * @param messageType
	 * @param senderAddress
	 * @param destinationAddress
	 * @param payload
	 * @throws IOException on error.
	 *********************************************************/
	
	public void sendAskSinMessage(String length, String messageCounter, String controlByte, String messageType, String senderAddress, String destinationAddress, String payload) throws IOException {
		// llnnccttssssssddddddpp.
		assertHex(length, 2);
		assertHex(messageCounter, 2);
		assertHex(controlByte, 2);
		assertHex(messageType, 2);
		assertHex(senderAddress, 6);
		assertHex(destinationAddress, 6);
			
		writeLine("As",
				length,
				messageCounter,
				controlByte,
				messageType,
				senderAddress,
				destinationAddress,
				payload);
	}

	/**********************************************************
	 * Disables the Asksin mode.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void disableAskSinMode() throws IOException {
		writeLine("Ax");
	}

	/**********************************************************
	 * Gets the current battery charge status.
	 * CUR only
	 * @return the battery status
	 * @throws IOException on error.
	 *********************************************************/
	
	public int getBatteryRaw() throws IOException {
		writeLine("a01");
		return Integer.parseInt(readLine());
	}
	
	/**********************************************************
	 * Gets the current battery charge status.
	 * CUR only
	 * @return the remaining battery in percent.
	 * @throws IOException on error.
	 *********************************************************/
	
	public int getBatteryPercent() throws IOException {
		writeLine("a00");
		return Integer.parseInt(readLine());
	}
	
	/**********************************************************
	 * Sets the USB charge mode.
	 * CUR only
	 * @param mode the mode to set.
	 * @throws IOException on error.
	 *********************************************************/

	public void setUSBChargeMode(ChargeMode mode) throws IOException {
		if (mode.equals(ChargeMode.WITH_500ma)) {
			writeLine("a00", "01");
		} else {
			writeLine("a00", "00");
		}
	}
	
	/**********************************************************
	 * Enable / disable USB charging.
	 * @param on set to true to activate charging false to deactivate it. 
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setUSBCharging(boolean on) throws IOException {
		if (on) {
			writeLine("a01", "00");
		} else {
			writeLine("a01", "01");
		}
	}
	
	/**********************************************************
	 * Reboot the device
	 * @throws IOException on error.
	 *********************************************************/
	
	public void reboot() throws IOException {
		writeLine("B00");
	}
	
	/**********************************************************
	 * Reboot into the boot loader.
	 * @throws IOException on error.
	 *********************************************************/

	public void bootIntoBootloader() throws IOException {
		writeLine("B01");
	}

	/**********************************************************
	 * Reads the current clock date/time.
	 * @param mode the mode of the readout.
	 * @return one of or both: dd.mm.yyyy hh:mm
	 * @throws IOException on error.
	 *********************************************************/

	public String readClock(ClockMode mode) throws IOException {
		switch (mode) {
			case DATE_ONLY:
				writeLine("c", "01");
				break;
			case TIME_ONLY:
				writeLine("c", "02");
				break;
			case BOTH:
				writeLine("c", "03");
				break;
			default:
				throw new IllegalArgumentException("Unknown mode "+mode);
		}
		return readLine();
	}

	/**
	 * @throws IOException
	 */
	public void setClock(Date date) throws IOException {
		writeLine("C", date.toString()); // TODO fix me
	}

	/**
	 * Tuxradio Only
	 * @throws IOException
	 */
//	public void setDisplayText(String msg) throws IOException {
//		out.write('D');
//		// out.write(Integer.toHexString(reg)); // TODO 
//		out.write(EOM);
//	}

	/**
	 * CUR only
	 * @throws IOException
	 */
//	public void setDisplayText(String msg) throws IOException {
//		out.write('d');
//		// out.write(Integer.toHexString(reg)); // TODO 
//		out.write(EOM);
//	}
	
	/**********************************************************
	 * Read out the current network settings IP MAC.
	 * CUN/O only 
	 * @return the current set IP address and MAC address.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readCurrentIP() throws IOException {
		writeLine("Ec");
		return readLine();
	}

	/**********************************************************
	 * Set the ethernet debug level.
	 * CUN/O only 
	 * @param level either 1, 2, 3
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setDebugLevel(int level) throws IOException {
		if (level < 1 || level > 3) {
			throw new IllegalArgumentException("Must be 1 <= level <= 3");
		}
		writeLine("E", ""+level);
	}
	
	/**********************************************************
	 * Restart the network subsystem: DHCP and NTP	
	 * CUN/O only 
	 * @throws IOException on error.
	 *********************************************************/

	public void restartNetworkInterface() throws IOException {
		writeLine("Ei");
	}
	
	/**********************************************************
	 * Request update of the internal clock via NTP.
	 * CUN/O only 
	 * @throws IOException on error.
	 *********************************************************/
	
	public void triggerNtpUpdate() throws IOException {
		writeLine("En");
	}
	
	/**********************************************************
	 * Perform an EPROM factory reset.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void factoryReset() throws IOException {
		writeLine("e");
	}
	
	/**********************************************************
	 * Sends an raw command 
	 * @param command the command to send.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void sendRaw(String command) throws IOException {
		writeLine(command);
	}
	
	/**********************************************************
	 * Send an FS20 command.
	 * @param houseCode of the device
	 * @param deviceAddress the unique device address
	 * @param command the command token.
	 * @param timeSpec a optional time spec, may be null.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void sendFS20Message(String houseCode, String deviceAddress, String command, String timeSpec) throws IOException {
		// hhhhaacc or hhhhaaccee
		assertHex(houseCode, 4);
		assertHex(deviceAddress, 2);
		assertHex(command, 2);
		if (timeSpec != null) {
			assertHex(timeSpec, 2);
		}
		
		writeLine("F",
				houseCode,
				deviceAddress,
				command,
				((timeSpec != null)?timeSpec:""));
	}
	
	/**********************************************************
	 * Reads the currently set Infrared mode
	 * @return the IR mode settings currently effective.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String getInfraRedMode() throws IOException {
		writeLine("Ir");
		return readLine();
	}
	
	/**********************************************************
	 * Enables Infrared message listening.
	 * @param skipRepeatedMessages true to skip automatically repeating messages.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void enableInfraRedListening(boolean skipRepeatedMessages) throws IOException {
		writeLine("Ir", skipRepeatedMessages?"02":"01");
		readLine();
	}
	
	/**********************************************************
	 * Send an infrared command.
	 * @param command a hex command code of length 12.
	 * @throws IOException on error. 
	 *********************************************************/
	
	public void sendInfraRedCommand(String command) throws IOException {
		assertHex(command, 12);
		writeLine("Is", command);
	}

	/**********************************************************
	 * Disables listening for Infrared messages.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void disableInfraredListening() throws IOException {
		writeLine("Ir00");
	}

	/**********************************************************
	 * Changes the LED operation mode.
	 * @param mode to use.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setLEDMode(LEDMode mode) throws IOException {
		switch (mode) {
		case OFF:
			writeLine("l", "00");
			break;
		case ON:
			writeLine("l", "01");
			break;
		case BLINK:
			writeLine("l", "02");
			break;
		default:
			throw new IllegalArgumentException("Unknown mode "+mode);
		}
	}
	
	/**********************************************************
	 * Terminates the network session.
	 * CUN/O only 
	 * @throws IOException on error.
	 *********************************************************/
	
	public void terminateSession() throws IOException {
		writeLine("q");
	}

	/**********************************************************
	 * Gets a list of all files with their size.
	 * CUN/O only 
	 * @return the file listing.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readFileListing() throws IOException {
		writeLine("r", ".");
		return readLine();
	}

	
	/**
	 * CUN/O only 
	 * @throws IOException 
	 */
	public byte[] readFile(String fileName) throws IOException {
		out.write('r');
		out.write(fileName);
		out.write(EOM);
		return new byte[0]; // TODO
	}

//	/**
//	 * CUR only 
//	 * @throws IOException 
//	 */
//	public void sleep(int seconds) throws IOException {
//		writeLine("s", Integer.toHexString(seconds)); // TODO
//	}
	
	/**********************************************************
	 * Sends an FHT command. 
	 * @param houseCode of the device to communicate with.
	 * @param command the command to send.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void sendFHTCommand(String houseCode, String command) throws IOException {
		// HHHHxxxxxx
		assertHex(houseCode, 4);
		writeLine("T", houseCode, command);
	}
	
	/**********************************************************
	 * Sets an FHT house code. 
	 * @param houseCode of the CUL device to set, 
	 *        0000 to deactivate FTH processing. 
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setFHTHouseCode(String houseCode) throws IOException {
		assertHex(houseCode, 4);
		writeLine("T01", houseCode);
	}
	
	/**********************************************************
	 * Gets an FHT house code. 
	 * return houseCode of the device, 
	 *        0000 to deactivate FTH processing. 
	 * @return the current house code, 0000 = deactivated
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readFHTHouseCode() throws IOException {
		writeLine("T01");
		return readLine();
	}



	/**********************************************************
	 * Gets the current router info.
	 * CUN/O only 
	 * @return the own and the router ID both 1 byte hex.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readRouterInfo() throws IOException {
		writeLine("u");
		return readLine();
	}
	
	/**********************************************************
	 * Activate the router mode.
	 * CUN/O only 
	 * @param routerID
	 * @param relayID
	 * @throws IOException on error.
	 *********************************************************/
	
	public void activateRouterMode(int routerID, int relayID) throws IOException {
		writeLine("ui", ""+routerID, ""+relayID); // TODO
	}
	
	/**********************************************************
	 * Send a router message.
	 * CUN/O only 
	 * @param routerID
	 * @param relayID
	 * @param message
	 * @throws IOException on error.
	 *********************************************************/
	
	public void routerMessage(int routerID, int relayID, String message) throws IOException {
		writeLine("ui", ""+routerID, ""+relayID, message); // TODO
	}
	
	/**********************************************************
	 * Read the current version of the firmware.
	 * @return the version string.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readVersionInfo() throws IOException {
		writeLine("V");
		return readLine();
	}
	
	/**********************************************************
	 * Read the uptime of the device.
	 * @return the uptime in milliseconds.
	 * @throws IOException on error.
	 *********************************************************/
	
	public int readUptime() throws IOException {
		writeLine("t");
		return Integer.decode("0x"+readLine())*1000/125;
	}
	
	/**********************************************************
	 * Reads the current MAC address of the device.
	 * CUN/O only
	 * @return the MAC.
	 * @throws IOException one error.
	 *********************************************************/
	
	public String readMAC() throws IOException {
		writeLine("Rim");
		return readLine();
	}
	
	/**********************************************************
	 * Set the MAC address of the device.
	 * CUN/O only
	 * @param mac the MAC address as hex, with optional semicolon.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setMAC(String mac) throws IOException {
		writeLine("Wim", mac);
	}

	/**********************************************************
	 * Checks if DHCP is currently enabled.
	 * @return true if enabled else false.
	 * @throws IOException on error.
	 *********************************************************/
	
	public boolean isDHCPEnabled() throws IOException {
		writeLine("Rid");
		return Integer.parseInt(readLine()) > 0;
	}
	
	/**********************************************************
	 * Enable / disable DHCP.
	 * CUN/O only
	 * @param enabled true to enable DHCP.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setDHCPEnabled(boolean enabled) throws IOException {
		writeLine("Wid", enabled?"1":"0");
	}

	/**********************************************************
	 * Reads the currently used IP4 address.
	 * CUN/O only
	 * @return the IP address in use.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readIP() throws IOException {
		writeLine("Ria");
		return readLine();
	}
	
	/**********************************************************
	 * Set the IP4 address of the device.
	 * CUN/O only
	 * @param ip the IP4 address to set in decimal with dot separator.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setIP(String ip) throws IOException {
		writeLine("Wia", ip);
	}

	/**********************************************************
	 * Reads the currently used network gateway.
	 * CUN/O only
	 * @return the gateway address.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readGateway() throws IOException {
		writeLine("Rig");
		return readLine();
	}
	
	/**********************************************************
	 * Set the IP4 gateway of the device.
	 * CUN/O only
	 * @param gateway the IP4 address of the gateway in decimal with dot separator.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setGateway(String gateway) throws IOException {
		writeLine("Wig", gateway);
	}

	/**********************************************************
	 * Reads the currently set network mask.
	 * CUN/O only
	 * @return the network mask.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readNetworkMask() throws IOException {
		writeLine("Rin");
		return readLine();
	}
	
	/**********************************************************
	 * Set the IP4 network mask of the device.
	 * CUN/O only
	 * @param mask the IP4 address of the mask in decimal with dot separator.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setNetworkMask(String mask) throws IOException {
		writeLine("Win", mask);
	}

	/**********************************************************
	 * Reads the network port where CUN(O) is listening on.
	 * CUN/O only
	 * @return the port.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readNetworkPort() throws IOException {
		writeLine("Rip");
		return readLine();
	}
	
	/**********************************************************
	 * Set the IP4 network port of the device.
	 * CUN/O only
	 * @param port the IP4 port of the server.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setNetworkMask(int port) throws IOException {
		writeLine("Wip", ""+port);
	}

	/**********************************************************
	 * Reads the currently configured NTP server.
	 * CUN/O only
	 * @return the network address of the NTP server.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readNTPServer() throws IOException {
		writeLine("RiN");
		return readLine();
	}
	
	/**********************************************************
	 * Set the NTP server.
	 * CUN/O only
	 * @param server the IP4 address of the NTP server in decimal with dot separator.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setTPServer(String server) throws IOException {
		writeLine("WiN", server);
	}

	/**********************************************************
	 * Reads the time zone offset used for NTP.
	 * CUN/O only
	 * @return the offset in hours.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readTimeZoneOffset() throws IOException {
		writeLine("Rio");
		return readLine();
	}
	
	/**********************************************************
	 * Set the NTP time zone offset.
	 * CUN/O only
	 * @param offset the time zone offset to GMT.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void setTimeZoneOffset(int offset) throws IOException {
		writeLine("Wio", ""+offset); // TODO correct encoding
	}
	
	/**********************************************************
	 * Dumps the content of the given registers.
	 * @param reg the registers to dump.
	 * @return the register data.
	 * @throws IOException on error.
	 *********************************************************/

	public String readRegister(String register) throws IOException {
		writeLine("C", register);
		return readLine();
	}
	
	/**********************************************************
	 * Enables listening for SlowRF messages like FS20 messages.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void enableSlowRFListening() throws IOException {
		writeLine("X01");
	}
	
	/**********************************************************
	 * Enables listening for SlowRF messages like FS20 messages.
	 * @param mode the output mode for the messages.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void enableSlowRFListening(String mode) throws IOException {
		writeLine("X"+mode);
	}
	
	/**********************************************************
	 * Gets the current SlowRF listening mode.
	 * @return the mode currently active.
	 * @throws IOException on error.
	 *********************************************************/
	
	public String readSlowRFMode() throws IOException {
		writeLine("X");
		return readLine().split(" ")[0];
	}
	
	/**
	 * @param mode
	 * @throws IOException 
	 */
	/**********************************************************
	 * Disables the SlowRF listening.
	 * @throws IOException on error.
	 *********************************************************/
	
	public void disableSlowRFListening() throws IOException {
		writeLine("X00");
	}

	/**********************************************************
	 * Gets the remaining send time for RF messages.
	 * @return the remaining send time in milliseconds.
	 * @throws IOException
	 *********************************************************/
	
	public long readRemainingSendTime() throws IOException {
		writeLine("X");
		return Long.parseLong(readLine().split("[ ]+")[1])*10;
	}
	
	/**********************************************************
	 * Starts the listener mode that will process all incoming message.
	 *********************************************************/
	
	public void startMessageProcessing() {
		if (this.listenerThread == null) {
			this.listenerThread = this.threadFactory.newThread(
				new Runnable() {
				public void run() {
					processIncommingMessages();
				}
			});
			this.listenerThread.start();
		}
	}
	
	/**********************************************************
	 * Stops the processing of incoming messages.
	 *********************************************************/
	
	public void stopMessageProcessing() {
		if (this.listenerThread != null) {
			this.listenerThread.interrupt();
		}
	}

	/**********************************************************
	 * Read all input messages and pass them to the registered listeners.
	 *********************************************************/
	
	private void processIncommingMessages() {
		try {
			while (!this.listenerThread.isInterrupted()) {
				String data = readLine();
				notifyListener(data);
			}
		} catch (IOException e) {
			// TODO Need to send this to a logger instead.
			e.printStackTrace();
		} 
		this.listenerThread = null;
	}
	
	/**********************************************************
	 * Notify all listeners that new data has arrived.
	 * @param data of the message.
	 *********************************************************/
	
	private void notifyListener(String data) {
		for (IRawMessageProcessor l : JCulFW.this.listener) {
			l.parseInput(data);
		}
	}
	
	/**********************************************************
	 * Change the thread factory that is used to create the 
	 * message processor thread.
	 * @param factory to create threads with.
	 *********************************************************/
	
	public void setThreadFactory(ThreadFactory factory) {
		this.threadFactory = factory;
	}
	
	/**********************************************************
	 * Add a new message listener that will be notified about
	 * new messages.
	 * @param l the listener to add.
	 *********************************************************/
	
	public void addMessageProcessor(IRawMessageProcessor l) {
		this.listener.add(l);
	}
	
	/**********************************************************
	 * Removes a previously registered listener.
	 * @param l the listener to remove.
	 * @return true if it was removed, 
	 *         false if it was not registered previously.
	 *********************************************************/
	
	public boolean removeMessageProcessor(IRawMessageProcessor l) {
		return this.listener.remove(l);
	}

}
